初探 async await

前言

最近准备学习Koa,因此将JavaScript的异步操作又理了一遍,发现还是async await写起来比较直白,不愧是现在JavaScript异步操作的终极解决方案。

一、Async/Await的初识

Async/Await的含义

Async - 定义异步函数(async function someName(){…})

  • 自动把函数转换为 Promise
  • 当调用异步函数时,函数返回值会被 resolve 处理
  • 异步函数内部可以使用 await

Await - 暂停异步函数的执行 (var result = await someAsyncCall())

  • 当使用在 Promise 前面时,await 等待 Promise 完成,并返回 Promise 的结果
  • await 只能和 Promise 一起使用,不能和 callback 一起使用
  • await 只能用在 async 函数中

Async/Await 和 Generator

async 函数是ES6所提出的,本质上是Generator函数的语法糖,但它在以下四点做了长足的改进:

  • 内置执行器:Generator 函数的执行必须依靠执行器,而 async 函数则自带执行器,调用的方式和普通函数调用一样。
  • 语义化:较之于Generator 函数的 * 和 yield,async/await无疑是更为语义化。
  • 返回的值是Promise: async 函数返回值是Promise对象,及哦啊哈子与Generator 函数所返回的Lterator对象更加方便,可以直接使用 then() 方法进行链式调用。
  • 适用范围更广: co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值。

二、async 语法

首先 async 函数返回一个 Promise 对象,也就是说 async 函数内部 return 返回的值,会成为 then 方法回调函数的参数,等同于 return Promise.resolve(value)。

1
2
3
4
5
6
async function  f() {
return 'hello async'
};
f().then( (v) => console.log(v))

// hello async

其次 async 函数所返回的 Promise 对象,必须等到内部所有的 await 命令的 Promise 对象执行完成后,才会发生状态改变。

1
2
3
4
5
6
7
8
9
const delay = timeout => new Promise(resolve=> setTimeout(resolve, console.log(timeout) timeout))
async function f(){
await delay(1000)
await delay(2000)
await delay(3000)
return 'end'
}

f().then(v => console.log(v)) // 需要等待6秒后才会输出"end"

正常情况下,await 命令后面跟着的是 Promise ,如果不是的话,也会被转换成一个立即 resolve 的 Promise。

1
2
3
4
async function  f() {
return await 1
};
f().then( (v) => console.log(v)) // 1

最后如果 async 函数内部抛出异常,则会导致返回的 Promise 对象状态变为 reject 状态。抛出的错误而会被 catch 方法回调函数接收到。

1
2
3
4
5
async function e(){
throw new Error('error');
}
e().then(v => console.log(v))
.catch( e => console.log(e));

三、async和其他异步操作的比较

直接上代码:
首先定义一个可以获取 github user 的 fetch 方法:

1
2
3
4
5
6
7
8
9
10
function fetchUser() { 
return new Promise((resolve, reject) => {
fetch('https://api.github.com/users/srtian')
.then((data) => {
resolve(data.json())
}, (error) => {
reject(error)
})
})
}

Promise

1
2
3
4
5
6
7
8
9
function getUserByPromise() {
fetchUser()
.then((data) => {
console.log(data)
}, (error) => {
console.log(error)
}
}
getUserByPromise();

这样看起来使用 Promise 来进行异步操作好像非常不错,但有个问题是,一旦then变多了,代码将会变得非常冗长和复杂,且语义化不明显,代码流程不能很好的表示执行的流程。

Generator

1
2
3
4
5
6
7
8
9
10
11
12
function *fetchUserByGenerator() {
const user = yield fetchUser()
return user
}

const g = fetchUserByGenerator()
const result = g.next().value
result.then((v) => {
console.log(v)
}, (error) => {
console.log(error)
})

Generator 的方式解决了 Promise 的一些问题,流程更加直观、语义化。但是 Generator 的问题在于,函数的执行需要依靠执行器,每次都需要通过 g.next() 的方式去执行。而且虽然语义上有所进步,但 * 和 yield 明显还是不能满足我们对语义化直观观察的需要。

async

1
2
3
4
5
6
 async function getUserByAsync(){
let user = await fetchUser()
return user
}
getUserByAsync()
.then(v => console.log(v))

而 async/await 则很好的解决了上面两种异步操作的一些问题。首先使用同步的方法来写异步,代码非常清晰直观;其次使用async和await,在语义上非常好,一眼就能看出代码执行的顺序;最后 async 函数自带执行器,执行的时候无需手动加载。

四、其他

错误处理

除了上面的那些东西,Async 函数的错误处理也需要额外注意:

1
2
3
4
5
6
let a
async function f() {
await Promise.reject('error')
a = await 1
}
f().then(v => console.log(a))

上面的 a = await 1 没有执行,这是由于在 async 函数中,只要有一个 await 出现 reject 状态,那么后面的 await 都不会被执行。所以我们就需要使用 try/catch 来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
let a
async function correct() {
try {
await Promise.reject('error')
} catch (error) {
console.log(error)
}
a = await 1
return a
}

correct().then(v => console.log(a)) // 1

并行

当我们需要使用并行进行加载时,使用async可能可以实现,但这是很低效的比如这样:

1
2
3
4
await a()
await b()
await c()
await d()

换成回调就是这样的:

1
2
3
4
5
6
7
a(() => {
b(() => {
c(() => {
d()
})
})
})

然而我们发现,原始代码中,函数 c 可以与 a 同时执行,但 async/await 语法会让我们倾向于在 b 执行完后,再执行 c。

所以我们其实可以这样:

1
2
3
4
5
6
7
8
9
10
11
async function ab() {
await a()
b()
}

async function cd() {
await c()
d()
}

Promise.all([ab(), cd()])

参考资料:

https://segmentfault.com/a/1190000014753495

https://juejin.im/post/596e142d5188254b532ce2da